﻿// Copyright (c) 2009, ids-adi.org /////////////////////////////////////////////
// All rights reserved.
//------------------------------------------------------------------------------
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//     * Neither the name of the ids-adi.org nor the
//       names of its contributors may be used to endorse or promote products
//       derived from this software without specific prior written permission.
//------------------------------------------------------------------------------
// THIS SOFTWARE IS PROVIDED BY ids-adi.org ''AS IS'' AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL ids-adi.org BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
////////////////////////////////////////////////////////////////////////////////

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Reflection;
using log4net;
using org.iringtools.utility;
using System.IO;

namespace org.iringtools.library
{
  public class EntityGenerator
  {
    private List<string> NHIBERNATE_ASSEMBLIES = new List<string>() 
    { 
      "NHibernate.dll",
      "NHibernate.ByteCode.Castle.dll",
      "NHibernate.Mapping.Attributes.dll",
      "Iesi.Collections.dll"        
    };

    private DataDictionary _dataDictionary;
    private Dictionary<string, string> _entities;
    private Dictionary<string, string> _entityNames;
    private string _compilerVersion;
    private string _namespace;
    private string _assembly;
    private List<string> _referencedAssemblies;
    private string _path;
    private ILog _logger;

    public EntityGenerator()
    {
      _entities = new Dictionary<string, string>();
      _assembly = Assembly.GetExecutingAssembly().GetName().Name;
      _logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
    }

    public void Generate(DatabaseDictionary dbDictionary, string compilerVersion, string @namespace, List<string> referencedAssemblies, string path)
    {
      string tempPath = string.IsNullOrEmpty(path) ? "." : path;

      _compilerVersion = string.IsNullOrEmpty(compilerVersion) ? "v3.5" : compilerVersion;
      _namespace = string.IsNullOrEmpty(@namespace) ? "unknown" : @namespace;
      _referencedAssemblies = referencedAssemblies;
      _path = tempPath + @"\" + _namespace.Replace('.', '\\') + @"\";

      if (dbDictionary.tables != null)
      {
        // Initialize objects
        _entityNames = new Dictionary<string, string>();
        _dataDictionary = new DataDictionary();
        _dataDictionary.dataObjects = new List<DataObject>();

        foreach (Table table in dbDictionary.tables)
        {
          string entityName = string.IsNullOrEmpty(table.entityName) ? table.tableName : table.entityName;

          _entityNames.Add(table.tableName, entityName);
        }

        // Create entities
        foreach (Table table in dbDictionary.tables)
        {
          CreateEntity(table);

          // Create data dictionary
          DataObject dataObject = new DataObject();

          dataObject.objectName = _entityNames[table.tableName];
          dataObject.objectNamespace = _namespace;
          dataObject.dataProperties = new List<DataProperty>();

          // Add key properties
          foreach (Key key in table.keys)
          {
            DataProperty dataProperty = new DataProperty()
            {
              propertyName = string.IsNullOrEmpty(key.propertyName) ? key.columnName : key.propertyName,
              dataType = key.columnType.ToString()
            };

            dataObject.dataProperties.Add(dataProperty);
          }

          // Add column properties
          foreach (Column column in table.columns)
          {
            DataProperty dataProperty = new DataProperty()
            {
              propertyName = string.IsNullOrEmpty(column.propertyName) ? column.columnName : column.propertyName,
              dataType = column.columnType.ToString()
            };

            try
            {
              dataObject.dataProperties.Add(dataProperty);
            }
            catch (Exception duplicatePropertyException)
            {
              _logger.Warn(duplicatePropertyException.ToString());
            }
          }

          // Add data relationship
          if (table.associations != null)
          {
            dataObject.dataRelationships = new List<DataRelationship>();

            foreach (Association association in table.associations)
            {
              DataRelationship dataRelationship = new DataRelationship();
              string associatedEntityName = _entityNames[association.associatedTableName];

              dataRelationship.relatedObject = associatedEntityName;

              switch (association.GetType().Name)
              {
                case "OneToOneAssociation":
                  dataRelationship.cardinality = Cardinality.OneToOne;
                  break;

                case "OneToManyAssociation":
                  dataRelationship.cardinality = Cardinality.OneToMany;
                  break;

                case "ManyToOneAssociation":
                  dataRelationship.cardinality = Cardinality.ManyToOne;
                  break;
              }

              dataObject.dataRelationships.Add(dataRelationship);
            }
          }

          _dataDictionary.dataObjects.Add(dataObject);
        }

        try
        {
          CompileEntities();

          // Write entities to disk
          DirectoryInfo directoryInfo = Directory.CreateDirectory(_path);

          foreach (KeyValuePair<string, string> pair in _entities)
          {
            string filePath = _path + pair.Key + ".cs";
            Utility.WriteString(pair.Value, filePath);
          }

          // Write data dictionary to disk
          string dataDictionary = Utility.Serialize<DataDictionary>(_dataDictionary, true);
          Utility.WriteString(dataDictionary, _path + "DataDictionary.xml");
        }
        catch (Exception ex)
        {
          throw ex;
        }
      }
    }

    private void CreateEntity(Table table)
    {
      StringBuilder entityBuilder = new StringBuilder();
      string entityName = _entityNames[table.tableName];
      string keyClassName = entityName + "Id";

      entityBuilder.AppendLine("using System;");
      entityBuilder.AppendLine("using System.Collections.Generic;");
      entityBuilder.AppendLine("using NHibernate.Mapping.Attributes;");
      entityBuilder.AppendLine("using Iesi.Collections.Generic;");
      entityBuilder.AppendFormat("namespace {0}{1}", _namespace, Environment.NewLine);
      entityBuilder.AppendLine("{");  // begin namespace block

      if (table.keys.Count > 1)
      {
        entityBuilder.AppendLine("[Serializable]");
        entityBuilder.AppendFormat("public class {0}{1}", keyClassName, Environment.NewLine);
        entityBuilder.AppendLine("{"); // begin composite key class block

        foreach (Key key in table.keys)
        {
          string keyName = string.IsNullOrEmpty(key.propertyName) ? key.columnName : key.propertyName;

          entityBuilder.AppendFormat("  public {0} {1} {{ get; set; }}{2}", key.columnType, keyName, Environment.NewLine);
        }

        entityBuilder.AppendLine("  public override bool Equals(object obj)");
        entityBuilder.AppendLine("  {");
        entityBuilder.AppendLine("    bool equals = false;");

        entityBuilder.AppendLine("    if (obj != null)");
        entityBuilder.AppendLine("    {");

        for (int i = 0; i < table.keys.Count; i++)
        {
          string keyName = string.IsNullOrEmpty(table.keys[i].propertyName) ? table.keys[i].columnName : table.keys[i].propertyName;

          if (i == 0)
          {
            entityBuilder.Append("      equals = (");
          }
          else
          {
            entityBuilder.Append(" && ");
          }

          entityBuilder.AppendFormat("this.{0} == (({1})obj).{0}", keyName, keyClassName);
        }

        entityBuilder.AppendLine(");");
        entityBuilder.AppendLine("    }");

        entityBuilder.AppendLine("    return equals;");
        entityBuilder.AppendLine("  }");

        entityBuilder.AppendLine("  public override int GetHashCode()");
        entityBuilder.AppendLine("  {");
        entityBuilder.AppendLine("    int hashCode = 0;");

        for (int i = 0; i < table.keys.Count; i++)
        {
          string keyName = string.IsNullOrEmpty(table.keys[i].propertyName) ? table.keys[i].columnName : table.keys[i].propertyName;

          if (i == 0)
          {
            entityBuilder.Append("    hashCode = ");
          }
          else
          {
            entityBuilder.Append(" + ");
          }

          entityBuilder.AppendFormat("{0}.GetHashCode()", keyName);
        }

        entityBuilder.AppendLine(";");
        entityBuilder.AppendLine("    return hashCode;");
        entityBuilder.AppendLine("  }");
        entityBuilder.AppendLine("}");// end composite key class block
      }

      entityBuilder.AppendFormat("[Class(Lazy = {0}, Name = \"{1}.{2}, {3}\", Table = \"{4}\")]{5}",
        table.lazyLoading.ToString().ToLower(), _namespace, entityName, _assembly, table.tableName, Environment.NewLine);
      entityBuilder.AppendFormat("public class {0}{1}", entityName, Environment.NewLine);
      entityBuilder.AppendLine("{");  // begin class block

      if (table.keys.Count > 1)
      {
        entityBuilder.AppendFormat("[CompositeId(Name = \"Id\", Class = \"{0}.{1}, {2}\")]{3}",
          _namespace, keyClassName, _assembly, Environment.NewLine);

        for (int i = 0; i < table.keys.Count; i++)
        {
          string keyName = string.IsNullOrEmpty(table.keys[i].propertyName) ? table.keys[i].columnName : table.keys[i].propertyName;

          entityBuilder.AppendFormat("[KeyProperty({0}, Name = \"{1}\", Column = \"{2}\")]{3}",
            i + 1, keyName, table.keys[i].columnName, Environment.NewLine);
        }

        entityBuilder.AppendFormat("public virtual {0} Id {{ get; set; }}", keyClassName);
      }
      else if (table.keys[0].keyType != KeyType.foreign)
      {
        entityBuilder.AppendFormat("[Id(0, Name = \"Id\", Column = \"{0}\")]{1}", table.keys[0].columnName, Environment.NewLine);
        entityBuilder.AppendFormat("[Generator(1, Class = \"{0}\")]{1}", table.keys[0].keyType, Environment.NewLine);
        entityBuilder.AppendFormat("public virtual {0} Id {{ get; set; }}{1}", table.keys[0].columnType, Environment.NewLine);
      }

      if (table.columns != null)
      {
        foreach (Column column in table.columns)
        {
          string propertyName = string.IsNullOrEmpty(column.propertyName) ? column.columnName : column.propertyName;

          entityBuilder.AppendFormat("[Property(Name = \"{0}\", Column = \"{1}\")]{2}", propertyName, column.columnName, Environment.NewLine);
          entityBuilder.AppendFormat("public virtual {0} {1} {{ get; set; }}{2}", column.columnType, propertyName, Environment.NewLine);
        }
      }

      if (table.associations != null)
      {
        foreach (Association association in table.associations)
        {
          string associatedEntityName = _entityNames[association.associatedTableName];

          switch (association.GetType().Name)
          {
            case "OneToOneAssociation":
              OneToOneAssociation oneToOneAssociation = (OneToOneAssociation)association;

              if (table.keys[0].keyType == KeyType.foreign)
              {
                entityBuilder.AppendFormat("[Id(0, Name = \"Id\", Column = \"{0}\")]{1}", table.keys[0].columnName, Environment.NewLine);
                entityBuilder.AppendFormat("[Generator(1, Class = \"{0}\")]{1}", table.keys[0].keyType, Environment.NewLine);
                entityBuilder.AppendFormat("[Param(2, Name = \"property\", Content = \"{0}\")]{1}", associatedEntityName, Environment.NewLine);
                entityBuilder.AppendFormat("public virtual {0} Id {{ get; set; }}{1}", table.keys[0].columnType, Environment.NewLine);
              }

              if (oneToOneAssociation.constrained)
              {
                entityBuilder.AppendFormat("[OneToOne(Name = \"{0}\", Class = \"{1}.{0}, {2}\", Constrained = true)]{3}",
                  associatedEntityName, _namespace, _assembly, Environment.NewLine);
              }
              else
              {
                entityBuilder.AppendFormat("[OneToOne(Name = \"{0}\", Class = \"{1}.{0}, {2}\", Cascade = \"save-update\")]{3}",
                  associatedEntityName, _namespace, _assembly, Environment.NewLine);
              }

              entityBuilder.AppendFormat("public virtual {0} {0} {{ get; set; }}", associatedEntityName);
              break;

            case "OneToManyAssociation":
              OneToManyAssociation oneToManyAssociation = (OneToManyAssociation)association;

              entityBuilder.AppendLine("[Set(0, Cascade = \"all-delete-orphan\", Inverse = true, Lazy = CollectionLazy.True)]");
              entityBuilder.AppendFormat("[Key(1, Column = \"{0}\")]", oneToManyAssociation.associatedColumnName);
              entityBuilder.AppendFormat("[OneToMany(2, Class = \"{0}.{1}, {2}\")]", _namespace, associatedEntityName, _assembly);
              entityBuilder.AppendFormat("public virtual ISet<{0}> {0}List {{ get; set; }}", associatedEntityName);
              break;

            case "ManyToOneAssociation":
              ManyToOneAssociation manyToOneAssociation = (ManyToOneAssociation)association;

              entityBuilder.AppendFormat("[ManyToOne(Column = \"{0}\")]", manyToOneAssociation.columnName);
              entityBuilder.AppendFormat("public virtual {0} {0} {{ get; set; }}", associatedEntityName);
              break;
          }
        }
      }

      entityBuilder.AppendLine("}");  // end class block
      entityBuilder.AppendLine("}");  // end namespace block      
      _entities.Add(entityName, entityBuilder.ToString());
    }

    private void CompileEntities()
    {
      Dictionary<string, string> compilerOptions = new Dictionary<string, string>();
      compilerOptions.Add("CompilerVersion", _compilerVersion);

      CompilerParameters parameters = new CompilerParameters();
      parameters.GenerateExecutable = false;

      // Add executing assembly
      parameters.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().ManifestModule.Name);      

      // Add NHibernate assemblies
      foreach (string referencedAssembly in NHIBERNATE_ASSEMBLIES)
      {
        parameters.ReferencedAssemblies.Add(referencedAssembly);
      }

      // Add external assemblies
      if (_referencedAssemblies != null)
      {
        foreach (string referencedAssembly in _referencedAssemblies)
        {
          parameters.ReferencedAssemblies.Add(referencedAssembly);
        }
      }

      try
      {
        CSharpCodeProvider codeProvider = new CSharpCodeProvider(compilerOptions);
        CompilerResults results = codeProvider.CompileAssemblyFromSource(parameters, _entities.Values.ToArray());

        if (results.Errors.Count > 0)
        {
          throw new Exception(results.Errors.ToString());
        }
      }
      catch (Exception ex)
      {
        throw ex;
      }
    }
  }
}
